﻿unit clientMain;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, System.Generics.Collections, Vcl.Graphics, Vcl.Controls,
  Vcl.Forms, Vcl.Dialogs, idTCPClientPool,
  Vcl.ExtCtrls, Vcl.StdCtrls, IdGlobal, IdSocketHandle, IdBaseComponent,
  IdComponent, IdIPMCastBase, IdIPMCastClient, IdTCPConnection, IdTCPClient,
  IdCustomTCPServer, IdTCPServer, IdContext, common, IdScheduler,
  IdSchedulerOfThread, IdSchedulerOfThreadPool;

type
  hashRecord = record
    hash: uint32;
    enable: boolean;
  end;

  TClientForm = class(TForm)
    Panel1: TPanel;
    Panel2: TPanel;
    outputMemo: TMemo;
    Label1: TLabel;
    IPEdit: TEdit;
    ServerNameFindClient: TIdIPMCastClient;
    ClientFileService: TIdTCPServer;
    Label2: TLabel;
    FolderNameEdit: TEdit;
    OpenFolderButton: TButton;
    FileOpenDialog: TFileOpenDialog;
    aliveTimer: TTimer;
    logMemo: TMemo;
    ClientFileServiceThreadPool: TIdSchedulerOfThreadPool;
    procedure ServerNameFindClientIPMCastRead(Sender: TObject;
      const AData: TIdBytes; ABinding: TIdSocketHandle);
    procedure ClientFileServiceExecute(AContext: TIdContext);
    procedure OpenFolderButtonClick(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure aliveTimerTimer(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    inTrans: boolean;
    localIP: string;
    clientPool: TIdTCPClientPool;
    hashList: TArray<hashRecord>;
    hashCount: uint32;
    sliceDataServiceList: array of TThreadList<string>;
    procedure completeReport(const speed: extended);
    procedure receiveFile;
    procedure getFileInfo;
    procedure testCard;
    procedure checkFilePath;
    function getDataServerIP(sliceIndex: uint32): string;
    function addDataServerIP(sliceIndex: uint32): string;
    function getHash(sliceIndex: uint32): string;
    function getSliceData(sliceIndex: uint32; l: uint32;
      var data: TIdBytes): boolean;
  public
    { Public declarations }
  end;

var
  ClientForm: TClientForm;

implementation

{$R *.dfm}

uses
  buffer, IdException, System.strUtils, System.Threading,
  System.IOUtils,
  System.math;

var
  clientConnected: boolean;

procedure TClientForm.aliveTimerTimer(Sender: TObject);
begin
  var
  testClient := clientPool.GetConnection;
  testClient.BoundIP := localIP;
  try
    with testClient do
    begin
      try
        if connected then
          Disconnect;

        connect(serverIP, infoServicePort);
        IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
        IOHandler.WriteLn(common.aliveString);
      except
        on E: Exception do
          // Memo.lines.add(E.ToString)
            ;
      end;
    end;
  finally
    clientPool.ReleaseConnection(testClient);
  end;
end;

procedure TClientForm.checkFilePath;
begin
  try
    if not TDirectory.Exists(FilePath) then
      TDirectory.CreateDirectory(FilePath);
  except
    on E: Exception do
    begin
      logMemo.lines.add('checkFilePath: ' + E.ToString);
      exit;
    end;
  end;
end;

procedure TClientForm.ClientFileServiceExecute(AContext: TIdContext);
begin
  // 客户端文件数据服务过程
  AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
  var
    fs: TFileStream := nil;
  var
    data: TIdBytes;
  var
    l: int32;
  try
    try
      // logMemo.lines.add('发送数据给: ' + AContext.Binding.PeerIP);
      var
      s := AContext.Connection.IOHandler.ReadLn();
      var
      commandString := SplitString(s, ' ');
      if length(commandString) < 2 then
        exit;
      if commandString[0] = common.BlockString then
      begin
        var
          sliceIndex: uint32;
        if TryStrToUInt(commandString[1], sliceIndex) then
          if inRange(sliceIndex, 0, totalSlice - 1) then
          begin
            SetLength(data, sliceLength);
            if not readBuffer(sliceIndex, data, l) then
            begin
              fs := TFileStream.Create(FilePath + '\' + FileInfo.FileName,
                fmOpenRead or fmShareDenyNone);
              var
              p := sliceLength * sliceIndex;

              fs.Position := p;
              l := fs.Read(data[0], sliceLength);
              writeBuffer(sliceIndex, data, l);
            end;

            if AContext.Connection.connected then
              AContext.Connection.IOHandler.Write(data, l);

          end;
      end;
    except
      on E: EIdConnClosedGracefully do;
      on E: Exception do
      begin
        logMemo.lines.add('ClientFileService: ' + E.ToString);
      end;
    end;
  finally
    freeAndNil(fs);
    SetLength(data, 0);
  end;
end;

procedure TClientForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  ServerNameFindClient.Active := false;
  ClientFileService.Active := false;
end;

procedure TClientForm.FormCreate(Sender: TObject);
begin
  clientPool := TIdTCPClientPool.Create(maxClientPoolSize);
end;

procedure TClientForm.FormShow(Sender: TObject);
begin
  IPv6CardList := EnumIPv6Card;

  serverIP := '';
  clientConnected := false;
  with ServerNameFindClient do
  begin
    Active := false;
    MulticastGroup := common.MulticastGroup;
    DefaultPort := InfoCastPort;
    Active := true;
  end;

  with ClientFileService do
  begin
    DefaultPort := clientFileServicePort;
    ListenQueue := 128;
    MaxConnections := 128;
    Active := false;
  end;

  if FilePath <> '' then
    FolderNameEdit.Text := FilePath
  else
    FilePath := FolderNameEdit.Text;
  inTrans := false;
  hashCount := 0;
end;

procedure TClientForm.completeReport(const speed: extended);
begin
  if not clientConnected then
    exit;
  var
  addClient := clientPool.GetConnection;
  addClient.BoundIP := localIP;
  with addClient do
  begin
    try
      try
        if connected then
          Disconnect;

        connect(serverIP, infoServicePort);
        IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
        IOHandler.WriteLn(common.completeString + ' ' + speed.ToString);
        Disconnect;
      except
        on E: Exception do
          logMemo.lines.add('completeReport: ' + E.ToString);
      end;
    finally
      clientPool.ReleaseConnection(addClient);
    end;
  end;

end;

procedure TClientForm.getFileInfo;
begin
  if not clientConnected then
    exit;
  var
  fileInfoClient := clientPool.GetConnection;
  fileInfoClient.BoundIP := localIP;
  with fileInfoClient do
  begin
    try
      try
        if connected then
          Disconnect;
        connect(serverIP, infoServicePort);
        outputMemo.lines.add('连接到: ' + serverIP);
        IOHandler.WriteLn(common.FileInfoString);
        var
        resp := IOHandler.ReadLn();
        if resp = common.FileInfoString then
        begin
          resp := IOHandler.ReadLn();
          outputMemo.lines.add('    文件名: ' + resp);
          FileInfo.FileName := resp;
          resp := IOHandler.ReadLn();
          outputMemo.lines.add('      长度: ' + resp);
          FileInfo.size := StrToUInt64(resp);

          totalSlice := (FileInfo.size + sliceLength - 1) div sliceLength;
        end;
        Disconnect;
      except
        on E: Exception do
          logMemo.lines.add('getFileInfo: ' + E.ToString);
      end;
    finally
      clientPool.ReleaseConnection(fileInfoClient);
    end;
  end;

  SetLength(sliceDataServiceList, totalSlice);
  SetLength(hashList, totalSlice);
  for var i := 0 to totalSlice - 1 do
  begin
    sliceDataServiceList[i] := TThreadList<string>.Create;
    sliceDataServiceList[i].add(serverString);
    hashList[i].enable := false;
  end;
end;

function TClientForm.getHash(sliceIndex: uint32): string;
begin
  Result := '';
  var
  h := hashList[sliceIndex];
  if h.enable then
  begin
    Result := h.hash.ToString;
    exit;
  end;

  if not clientConnected then
    exit;
  var
  addClient := clientPool.GetConnection;
  addClient.BoundIP := localIP;
  with addClient do
  begin
    try
      try
        if connected then
          Disconnect;

        connect(serverIP, infoServicePort);
        IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
        IOHandler.WriteLn(common.hashString + ' ' + sliceIndex.ToString);
        var
        respString := SplitString(IOHandler.ReadLn(), ' ');
        if length(respString) < 2 then
          exit;
        if respString[0] = hashString then
        begin
          Result := respString[1];
          var
            v: uint32;
          if TryStrToUInt(Result, v) then
          begin
            hashList[sliceIndex].hash := v;
            hashList[sliceIndex].enable := true;
          end;
        end;
      except
        on E: Exception do
          logMemo.lines.add('getHash: ' + E.ToString);
      end;
    finally
      clientPool.ReleaseConnection(addClient);
    end;
  end;
end;

function TClientForm.addDataServerIP(sliceIndex: uint32): string;
begin
  if not clientConnected then
    exit;
  var
  addClient := clientPool.GetConnection;
  addClient.BoundIP := localIP;
  with addClient do
  begin
    try
      try
        if connected then
          Disconnect;

        connect(serverIP, infoServicePort);
        IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
        // memo.lines.add('连接到: '+serverIP);
        IOHandler.WriteLn(common.addString + ' ' + sliceIndex.ToString);
        Disconnect;
      except
        on E: Exception do
          logMemo.lines.add('addDataServiceIP: ' + E.ToString);
      end
    finally
      clientPool.ReleaseConnection(addClient);
    end;
  end;
end;

function TClientForm.getDataServerIP(sliceIndex: uint32): string;
begin
  Result := serverString;
  var
  l := sliceDataServiceList[sliceIndex].LockList;
  if l.Count > 1 then
  begin
    var
    t := random(l.Count);
    Result := l[t];
  end;
  sliceDataServiceList[sliceIndex].unLockList;
end;

procedure TClientForm.ServerNameFindClientIPMCastRead(Sender: TObject;
  const AData: TIdBytes; ABinding: TIdSocketHandle);
begin
  var
  castString := SplitString(BytesToString(AData), ' ');
  if length(castString) < 1 then
    exit;
  case castCommandList.IndexOf(castString[0]) of
    0: // 多播地址发现
      if not clientConnected then
      begin
        clientConnected := true;
        serverIP := ABinding.PeerIP;
        IPEdit.Text := serverIP;
        outputMemo.lines.Clear;
        outputMemo.lines.add('发现服务端, IP: ' + serverIP);
        Application.ProcessMessages;
        testCard;
        getFileInfo;
      end;
    1: // 文件校验和广播
      begin
        if length(castString) < 3 then
          exit;
        var
          sliceIndex: uint32;
        var
          hashValue: uint32;

        if TryStrToUInt(castString[1], sliceIndex) then
          if TryStrToUInt(castString[2], hashValue) then
          begin
            hashList[sliceIndex].hash := hashValue;
            hashList[sliceIndex].enable := true;
            inc(hashCount);
          end;
      end;
    2: // 文件slice所在主机广播
      begin
        if length(castString) < 3 then
          exit;
        var
          sliceIndex: uint32;
        if TryStrToUInt(castString[1], sliceIndex) then
        begin
          var
          l := sliceDataServiceList[sliceIndex].LockList;
          if not l.Contains(castString[2]) then
            l.add(castString[2]);
          sliceDataServiceList[sliceIndex].unLockList;
        end;
      end;
    3: // 准备接受数据
      if not inTrans then
      begin
        inTrans := true;
        TThread.CreateAnonymousThread(receiveFile).Start;
      end;
  end;
end;

procedure TClientForm.OpenFolderButtonClick(Sender: TObject);
begin
  if FileOpenDialog.Execute then
  begin
    FilePath := FileOpenDialog.FileName;
    FolderNameEdit.Text := FilePath;
  end;
end;

function TClientForm.getSliceData(sliceIndex: uint32; l: uint32;
  var data: TIdBytes): boolean;
begin
  Result := false;
  var
  fileDataClient := clientPool.GetConnection;
  fileDataClient.BoundIP := localIP;
  try
    try
      with fileDataClient do
      begin
        if connected then
          Disconnect;

        var
        dataServiceIP := getDataServerIP(sliceIndex);
        // logMemo.lines.add('随机选择一个主机, 地址: ' + dataServiceIP);
        if dataServiceIP = serverString then
          connect(serverIP, serverFileServicePort)
        else
          connect(dataServiceIP, clientFileServicePort);
        // logMemo.lines.add('    链接到主机');

        IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
        IOHandler.WriteLn(BlockString + ' ' + sliceIndex.ToString);

        IOHandler.ReadBytes(data, l, false); // Append=False，禁用附加模式

        var
        hash1 := getHash(sliceIndex);
        var
        hash2 := hashData(data, l).ToString;
        Result := hash1 = hash2;

        if Result then
        begin
          writeBuffer(sliceIndex, data, l);
          // logMemo.lines.add(sliceIndex.ToString + ' 已接受');
        end
      end;
    except
      on E: EIdConnClosedGracefully do;
      on E: Exception do
      begin
        logMemo.lines.add('  getSliceData ' + sliceIndex.ToString + ' :  ' +
          E.ToString);
        Result := false;
      end
    end
  finally
    clientPool.ReleaseConnection(fileDataClient);
  end;
end;

procedure TClientForm.receiveFile;
begin
  outputMemo.lines.add('已保存hash数据: ' + hashCount.ToString);
  outputMemo.lines.add('----------开始接收文件----------');
  ClientFileService.Active := true;

  // aliveTimer.Enabled := true;
  if FileInfo.FileName <> '' then
  begin
    checkFilePath;
    var
    t0 := now;
    var
      len: uint64 := 0;
    var
      fs: TFileStream;
    var
      block: TIdBytes;
    var
      rArray: TArray<uint32>;
    SetLength(block, slicesOfBlock * sliceLength);

    var
    sizeString := FormatNumberWithCommas(FileInfo.size);

    try
      try
        if FileExists(FilePath + '\' + FileInfo.FileName) then
          fs := TFileStream.Create(FilePath + '\' + FileInfo.FileName,
            fmOpenReadWrite or fmShareDenyNone)
        else
          fs := TFileStream.Create(FilePath + '\' + FileInfo.FileName,
            fmCreate or fmOpenReadWrite or fmShareDenyNone);
        var
        fileLength := fs.size;
        var
          sliceIndex: uint32 := 0;
        outputMemo.lines.add('传送进度: ' + FormatNumberWithCommas(len) + '/' +
          sizeString);
        var
        progressPostion := outputMemo.lines.Count - 1;
        if fileLength > FileInfo.size then // 超过传送文件大小的部分截断
        begin
          outputMemo.lines.add('  已存在同名文件，但是长度大于要传送的文件，已经截断到预期长度');
          fs.size := FileInfo.size;
          fs.Seek(0, soFromBeginning);
          fileLength := FileInfo.size;
        end;

        // 文件已经存在的数据校验
        if fileLength > 0 then
        begin
          outputMemo.lines.add('  已存在同名文件，开始校验数据。。。');
          var
            data: TIdBytes;
          SetLength(data, sliceLength);

          while len < fileLength do
          begin
            var
            realLength := min(sliceLength, FileInfo.size - len);
            var
            prevpos := fs.Position;
            var
            readLength := fs.Read(data, realLength);
            if readLength <> realLength then
            begin
              fs.Seek(prevpos, soFromBeginning);
              break;
            end;
            var
            hash1 := getHash(sliceIndex);
            var
            hash2 := hashData(data, realLength).ToString;
            if hash1 <> hash2 then
            begin
              fs.Seek(prevpos, soFromBeginning);
              break;
            end;
            writeBuffer(sliceIndex, data, realLength);
            addDataServerIP(sliceIndex);
            inc(sliceIndex);
            inc(len, realLength);
          end;
        end;

        SetLength(rArray, slicesOfBlock);
        TThreadPool.Default.SetMaxWorkerThreads(8);

        // 尚未获取的数据
        outputMemo.lines.add('  开始接收数据。。。');
        while len < FileInfo.size do
        begin

          var
          realBlockLength := min(slicesOfBlock * sliceLength,
            FileInfo.size - len);
          // 本次获取数据长度
          var
          lastSliceLength := sliceLength; // 本次最后一个slice长度
          if realBlockLength < slicesOfBlock * sliceLength then
            lastSliceLength := realBlockLength mod sliceLength;

          // 一次获取多个数据
          var
          realSliceCount := (realBlockLength + sliceLength - 1) div sliceLength;
          // 需要获取的数据的slice数

          for var i := 0 to realSliceCount - 1 do
            rArray[i] := i;
          randomArray(rArray, realSliceCount);
          var
            data: TIdBytes;
          SetLength(data, sliceLength);
          for var i := 0 to realSliceCount - 1 do
          begin
            var
              singleLength: uint32; // 单次取的数据长度

            if rArray[i] = realSliceCount - 1 then
              singleLength := lastSliceLength
            else
              singleLength := sliceLength;

            while not getSliceData(sliceIndex + rArray[i],
              singleLength, data) do;

            writeBuffer(sliceIndex + rArray[i], data, singleLength);
            addDataServerIP(sliceIndex + rArray[i]);
            copyTIdBytes(data, 0, block, rArray[i] * sliceLength, singleLength);
          end;

          fs.Write(block[0], realBlockLength);
          inc(len, realBlockLength);
          outputMemo.lines[progressPostion] := '传送进度: ' + FormatNumberWithCommas
            (len) + '/' + sizeString;
          inc(sliceIndex, realSliceCount);
        end;
      except
        on E: Exception do
        begin
          logMemo.lines.add('  receiveFile: ' + E.ToString);
          exit;
        end;
      end;

    finally
      freeAndNil(fs);
    end;
    var
    t1 := now;
    var
    t := (t1 - t0) * 86400;
    var
    BPS := FileInfo.size / 1048576.0 / t;
    outputMemo.lines.add('接收完毕, 速度 ' + Format('%.4f', [BPS]) + ' MB/s');
    completeReport(BPS);
    SetLength(block, 0);
  end;
end;

procedure TClientForm.testCard;
begin
  var
  find := false;
  outputMemo.lines.add('查找与服务端连接的IP...');
  var
  testClient := clientPool.GetConnection;
  try
    with testClient do
    begin
      IPVersion := ID_IPv6;
      ConnectTimeout := 2000;
      readTimeout := 2000;
      for var i := 0 to IPv6CardList.Count - 1 do
      begin
        BoundIP := IPv6CardList[i].IPv6;
        outputMemo.lines.add('   测试本地IP: ' + BoundIP);
        try
          connect(serverIP, infoServicePort);
          IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
          IOHandler.WriteLn(common.testString);
          var
          resp := IOHandler.ReadLn;
          if resp = common.testOKString then
          begin
            localIP := IPv6CardList[i].IPv6;
            outputMemo.lines.add('找到与服务端连接的本地IP: ' + localIP);
            find := true;
            break;
          end;
        except
          on E: Exception do
            logMemo.lines.add(E.ToString);
        end;
      end;
    end;
  finally
    clientPool.ReleaseConnection(testClient);
  end;
  if not find then
    outputMemo.lines.add('未发现与服务端连接网卡？发生错误？');
end;

end.
